In [3]:
import sys
sys.path.append('..')
import random
In this example we are going to build a very simple and useless algorithm to explore the possibilities of the pynetics library.
Our problem will be as follows. We'll going to develop a genetic algorithm to find the what binary list of lenght $L=N$ is the one with the bigger sum. Yes, total absurd, but useful to learn GAs and this library.
Let's start from the begining. The individuals.
The individuals are the most important component in a genetic algorithm. Each individual is a possible solution, good or bad, for our problem.
We want to model an individual capable of represent a possible solution for our problem. Pynetics have a perfect representation for this problem, the BinaryIndividual
, so it's no necessary to create a custom individual. We'll cross that bridge when we get to it.
The algorithm will create individuals using a SpawningPool
implementation. We're going to use a special implementation inside the module pynetics.ga_bin
called BinaryIndividualSpawningPool
, that creates BinaryIndividual
instances of the given size.
In [4]:
from pynetics.ga_bin import BinaryIndividualSpawningPool
# Let's define the size of our individuals (the numer of 1's and 0's)
individual_size = 25
binary_individual_spawning_pool=BinaryIndividualSpawningPool(size=individual_size)
Now the spawning pool will be capable of creating individuals of the specified size. The genetic algorithm will create a population of individuals using the spawn
method to populate it. We'll also specify a population size for the algorithm and see an example of population:
In [11]:
population_size = 10
for i in range(population_size):
individual = binary_individual_spawning_pool.spawn()
print(i, '->', individual)
Our individuals are solutions for the problem but, ¿how can we measure how good or how bad are they? That is what the fitness
is for. It's a function that will return a float value. The bigger the value, the better the individual is.
We could use a fitness function equals to the sum of al $1$'s but if we want to stop the algorithm based on the fitness, is not the same the best fitness for an individual of size 10 than an individual of size 20.
So the fitness funcion we're gonna use is a function with the form $1 / (1 + \alpha)$, being $\alpha$ the error of our individual. The error will be computed as the number of $0$'s the individual has.
In [12]:
def maximize_ones_fitness(individual):
error = len(individual) - sum(individual)
return 1 / (1 + error)
This function guarantees that the fitness will belong to the $(0, 1]$ interval. Let's see an example of how it behaves.
In [17]:
for i in range(population_size):
individual = binary_individual_spawning_pool.spawn()
fitness = maximize_ones_fitness(individual)
print(i, '->', individual, fitness)
In [18]:
from pynetics.stop import FitnessBound
fitness_stop_condition = FitnessBound(1)
Instances of the class FitnessBound are created by specifying the fitness thresshold above which we can stop our algorithm. We have specified a FitnessBound object with a thressholdd of $1$. That means that all the values below $1$ will not stop our algorithm whereas all the values upper or equal than $1$ will do.
Because our fitness value belongs to the $(0, 1]$ interval, the algorithm will stop only when the population have an individual with a fitness of $1$ (all $1$'s).
For our GA, we're going to use a tournament selection. Tournament selection works by selecting $n$ individuals randomly from the population and then returning the best of then (based on their fitnesses).
In [19]:
from pynetics.selections import Tournament
tournament_selection = Tournament(2)
Now the recombination, i.e. the step where the individuals are selected and their genetic informatio is going to be inherited by their progeny.
We'll use a OnePointRecombination
, included in the module ga_list
. Also, for the recommender we'll going to specify the probability for two individuals to mate to be 1, that is, they always mate.
In [20]:
from pynetics.ga_list import OnePointRecombination
recombination_probability = 1
recombination = OnePointRecombination()
In [21]:
from pynetics.ga_bin import AllGenesCanSwitch
mutation_probability = 1 / individual_size
mutation = AllGenesCanSwitch()
Once we've got the offspring, we need to replace the population with these new borns. The operator for that matter will be a LowElitism
operator, where the worst individuals of the popularin are replaced by the offspring.
We'll fix the replacement rate in $0.9$, i.e. a $90\%$ of the pooulation will be replaced for each iteration of the loop.
In [22]:
from pynetics.replacements import LowElitism
replacement_rate = 0.9
replacement = LowElitism()
In [42]:
from pynetics.algorithms import SimpleGA
ga = SimpleGA(
stop_condition=fitness_stop_condition,
population_size=population_size,
fitness=maximize_ones_fitness,
spawning_pool=binary_individual_spawning_pool,
selection=tournament_selection,
recombination=recombination,
mutation=mutation,
replacement=replacement,
p_recombination=recombination_probability,
p_mutation=mutation_probability,
replacement_rate=replacement_rate,
)
Now we've created our algorithm, can run it to find the right solution. Let's see how it works
In [43]:
ga.run()
print(ga.best())
We can specify functions to be executed while the training takes place. The next example adds some of those functions.
In [44]:
ga.on_start(
lambda ga: print('Starting genetic algoritm')
).on_end(
lambda ga: print('Genetic Algorithm ended. Best individual:', ga.best())
).on_step_start(
lambda ga: print('Step:', ga.generation, '->', end='')
).on_step_end(
lambda ga: print(ga.best(), 'fitness:', ga.best().fitness())
)
ga.run()
And here ends the quickstart tutorial!
In [ ]: